﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;
using System.Collections.Specialized;
using System.Web;

using Mr.HttpBodyWrapper;

namespace OAuth
{
    public class OAuthConsumer
    {
        #region Constructor
        private OAuthConsumer()
            : this(string.Empty, string.Empty, string.Empty)
        { }

        public OAuthConsumer(string consumerKey, string consumerSecret)
            : this(consumerKey, consumerSecret, string.Empty)
        { }

        public OAuthConsumer(string consumerKey, string consumerSecret, string callbackUrl)
        {
            this.ConsumerKey = consumerKey;
            this.ConsumerSecret = consumerSecret;
            this.CallbackUrl = callbackUrl;
        }

        #endregion
        #region Properties

        public string ConsumerKey { get; set; }

        public string ConsumerSecret { get; set; }

        public string CallbackUrl { get; set; }

        public ServiceProvider ServiceProvider { get; set; }

        public string Token { get; set; }

        public string TokenSecret { get; set; }

        public string Verifier { get; set; }

        public bool Initialized { get; private set; }

        #endregion
        /// <summary>
        /// get the request token from service provider
        /// if the final access token and secret have been there, just return them.
        /// </summary>
        /// <returns>
        /// a NameValueCollection contains
        /// keys:
        ///     oauth_token
        ///     oauth_token_secret
        ///     oauth_callback_confirmed
        /// </returns>
        public IDictionary<string, string> GetRequestToken(params OAuthQueryParameter[] queryParameters)
        {
            if (this.Initialized)
            {
                // return the final access token and token secret
                return this.GetFinalAccessTokenAndSecret();
            }

            #region Prepare the Signature and Headers
            string normalizedUrl = string.Empty, normalizedRequestParameters = string.Empty;
            string timestamp = OAuthUtility.Instance.GenerateTimeStamp();
            string nonce = OAuthUtility.Instance.GenerateNonce();
            string callback = OAuthUtility.Instance.UrlEncode(this.CallbackUrl);
            string baseURI = this.ServiceProvider.OAuthGetRequestTokenUrl;

            string postData = string.Empty; ;
            List<OAuthQueryParameter> queryParams = null;
            if (queryParameters != null && queryParameters.Length > 0)
            {
                queryParams = queryParameters.ToList();
                postData = OAuthUtility.Instance.NormalizeRequestParameters(queryParams);
                queryParams.ForEach(
                    delegate(OAuthQueryParameter qp){
                        qp.Value = OAuthUtility.Instance.UrlEncode(qp.Value);
                        qp.InBody = true;
                    }
                );
            }

            // don't need verifier but callback
            string signature =
                OAuthUtility.Instance.GenerateSignature(
                    new Uri(baseURI),
                    this.ConsumerKey,
                    this.ConsumerSecret,
                    string.Empty, // == ""
                    string.Empty, // == ""
                    null,
                    OAuthHttpMethod.POST.ToString(),
                    timestamp,
                    nonce,
                    callback,
                    SignatureTypes.HMACSHA1,
                    out normalizedUrl,
                    out normalizedRequestParameters,
                    queryParams == null ? null : queryParams.ToArray<OAuthQueryParameter>());

            signature = OAuthUtility.Instance.UrlEncode(signature);

            StringBuilder headerBuilder = new StringBuilder();
            string format = "{0}=\"{1}\",";
            headerBuilder
                .AppendFormat(format, OAuthUtility.OAuthVersionKey, OAuthUtility.OAuthVersion)
                .AppendFormat(format, OAuthUtility.OAuthNonceKey, nonce)
                .AppendFormat(format, OAuthUtility.OAuthTimestampKey, timestamp)
                .AppendFormat(format, OAuthUtility.OAuthConsumerKeyKey, this.ConsumerKey)
                .AppendFormat(format, OAuthUtility.OAuthSignatureMethodKey, OAuthUtility.HMACSHA1SignatureType)
                .AppendFormat(format, OAuthUtility.OAuthSignatureKey, signature);

            // the callback property is not neccesary
            // 1. need callback, it is for web and part installed application
            // 2. don't need callback, for example, t.sina.com api, callback property is decided by api provider
            if (!string.IsNullOrEmpty(this.CallbackUrl))
            {
                headerBuilder.AppendFormat(format, OAuthUtility.OAuthCallbackKey, callback);
            }
            // remove last , symbol
            headerBuilder.Remove(headerBuilder.Length - 1, 1);

            //
            // create headers
            IDictionary<string, string> headers = new Dictionary<string, string>();
            headers["Authorization"] = string.Format("OAuth {0}",headerBuilder.ToString());
            headers["Content-Type"] = "application/x-www-form-urlencoded";

            byte[] bytes = Encoding.UTF8.GetBytes(postData);

            #endregion
            OAuthMessage message = OAuthMessage.ForPost(new Uri(baseURI), headers, bytes);
            OAuthResponse response = this.Request(message);
            IDictionary<string, string> keyValues = this.ReadtoKeyValuesFromResponse(response);
            this.Token = keyValues[OAuthUtility.OAuthTokenKey];
            this.TokenSecret = keyValues[OAuthUtility.OAuthTokenSecretKey];
            return keyValues;
        }

        public void AuthorizeToken(Action<OAuthConsumer> authorize)
        {
            if (this.Initialized)
            {
                // if the accessor have been initialized
                // the process of authorizing is not neccessary.
                return;
            }

            if (string.IsNullOrEmpty(this.Token)
                || string.IsNullOrEmpty(this.TokenSecret))
            {
                throw new ApplicationException(
                    "Before authorize the access token, the method GetRequestToken should be called.");
            }
            try
            {
                authorize(this);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw ex;
            }
        }

        /// <summary>
        /// get the access token from service provider,
        /// if the access token have been there, just return them.
        /// </summary>
        /// <returns>
        /// a NameValueCollection contains
        /// keys:
        ///     oauth_token
        ///     oauth_token_secret
        /// </returns>
        public IDictionary<string, string> GetAccessToken()
        {
            if (this.Initialized)
            {
                // return the final access token and token secret
                return GetFinalAccessTokenAndSecret();
            }

            if (string.IsNullOrEmpty(this.Token)
                || string.IsNullOrEmpty(this.Verifier))
            {
                throw new ApplicationException("The token or the verifier is null or empty.");
            }
            #region Prepare the Signature and Headers
            string timestamp = OAuthUtility.Instance.GenerateTimeStamp();
            string nonce = OAuthUtility.Instance.GenerateNonce();
            string baseUri = this.ServiceProvider.OAuthGetAccessTokenUrl;
            string token = OAuthUtility.Instance.UrlEncode(this.Token);
            string verifier = OAuthUtility.Instance.UrlEncode(this.Verifier);

            // need verifier, do not need callback url
            string normalizedUrl = string.Empty, normalizedRequestParameters = string.Empty;
            string signature =
                OAuthUtility.Instance
                .GenerateSignature(
                    new Uri(baseUri),
                    this.ConsumerKey,
                    this.ConsumerSecret,
                    token,
                    this.TokenSecret,
                    verifier,
                    OAuthHttpMethod.GET.ToString(),
                    timestamp,
                    nonce,
                    null,
                    SignatureTypes.HMACSHA1,
                    out normalizedUrl,
                    out normalizedRequestParameters);

            signature = OAuthUtility.Instance.UrlEncode(signature);

            StringBuilder headerBuilder = new StringBuilder();
            headerBuilder
                .Append("oauth_version=\"" + OAuthUtility.OAuthVersion + "\", ")
                .Append("oauth_nonce=\"" + nonce + "\", ")
                .Append("oauth_timestamp=\"" + timestamp + "\", ")
                .Append("oauth_consumer_key=\"" + this.ConsumerKey + "\", ")
                .Append("oauth_verifier=\"" + verifier + "\", ")
                .Append("oauth_token=\"" + token + "\", ")
                .Append("oauth_signature_method=\"" + OAuthUtility.HMACSHA1SignatureType + "\", ")
                .Append("oauth_signature=\"" + signature + "\"");

            IDictionary<string, string> headers = new Dictionary<string, string>();
            headers["Authorization"] = string.Format("OAuth {0}", headerBuilder.ToString());
            #endregion

            OAuthMessage message = OAuthMessage.ForGet(new Uri(baseUri), headers);
            try
            {
                OAuthResponse response = this.Request(message);
                IDictionary<string, string> keyValues = this.ReadtoKeyValuesFromResponse(response);
                this.Token = keyValues[OAuthUtility.OAuthTokenKey];
                this.TokenSecret = keyValues[OAuthUtility.OAuthTokenSecretKey];
				
                // initialize completed
                this.Initialized = true;
                return keyValues;
            }
            catch (WebException webException)
            {
                Console.WriteLine(webException.Message);
                return null;
            }
        }
        private IDictionary<string, string> ReadtoKeyValuesFromResponse(OAuthResponse response)
        {
            // for instance
            // 1.get the request token
            // oauth_token=4%2F-OSUbRvBfleYOfcDYEfWn18Qqggn&oauth_token_secret=BSIGUTOrX4SZSSa0zq5hdnL0&oauth_callback_confirmed=true
            // 2.get the access token
            // oauth_token=1%2FBNjErPT-XlpdNyCaN9nRidjXZusXimKX8-LbUG2L3u4&oauth_token_secret=y29fp6BRSW3yKYENwotpQE0j
            string responseText = string.Empty;
            using (Stream output = response.GetResponseStream())
            {
                using (StreamReader reader = new StreamReader(output))
                {
                    responseText = reader.ReadToEnd();
                    // Console.WriteLine(responseText);
                }
            }
            IDictionary<string, string> keyValues = this.GetKeyValuesFromResponseText(responseText);
            return keyValues;
        }

        /// <summary>
        /// Convert to Name value format from string
        /// the string format should be like "a=b&c=d"
        /// </summary>
        /// <param name="responseText">the target string</param>
        /// <returns>a key value collection contains the keys and values from ""a=b&c=d""</returns>
        private IDictionary<string, string> GetKeyValuesFromResponseText(string responseText)
        {
            string[] resultArr = responseText.Split('&');

            IDictionary<string, string> keyValues = new Dictionary<string, string>();
            foreach (string str in resultArr)
            {
                string[] items = str.Split('=');
                keyValues.Add(items[0], items[1]);
            }
            return keyValues;
        }

        /// <summary>
        /// Send a http call using the information from OauthMessage object
        /// after construct a new request object, get the response from server
        /// </summary>
        /// <param name="message">the target message which contains Http method, headers, content....</param>
        /// <returns>the response from server</returns>
        private OAuthResponse Request(OAuthMessage message)
        {
            OAuthRequest request = new OAuthRequest(message);
            OAuthResponse response = request.GetResponse();
            return response;
        }

        private void Request(OAuthMessage message, Action<OAuthResponse> action)
        {
            OAuthRequest request = new OAuthRequest(message);
            request.BeginGetResponse(action);
        }
        /// <summary>
        /// if the access have been initialized
        /// GetRequsetToken, GetAccessToken the 2 methods should not be called.
        /// if call these methods, just return the final access token and token secret
        /// </summary>
        /// <returns>the final token and secret</returns>
        private IDictionary<string, string> GetFinalAccessTokenAndSecret()
        {
            IDictionary<string, string> accessTokenKeyValues = new Dictionary<string, string>();
            // the token is the final access token
            accessTokenKeyValues.Add(OAuthUtility.OAuthTokenKey, this.Token);
            // the token secret is the final access token secret
            accessTokenKeyValues.Add(OAuthUtility.OAuthTokenSecretKey, this.TokenSecret);
            return accessTokenKeyValues;
        }

        public void LoadAccessTokenAndSecret(string accessToken, string tokenSecret)
        {
            this.Token = accessToken;
            this.TokenSecret = tokenSecret;
            this.Initialized = true;
        }

        public void AsynInvokeAPI(
            Uri api,
            OAuthHttpMethod method,
            IHttpBodyWrapper container,
            Action<OAuthResponse> action)
        {
            if (this.Initialized)
            {
                OAuthMessage message = this.NewMessage(api, method, container);
                this.Request(message, action);
            }
        }

        public void AsynInvokeAPI(
            Uri api,
            OAuthHttpMethod method,
            Action<OAuthResponse> action,
            params QueryParameter[] queryParameters)
        {
            if (this.Initialized)
            {
                OAuthMessage message = this.NewMessage(api, method, new KeyValuesHttpBodyWrapper(queryParameters));
                this.Request(message, action);
            }
        }

        public OAuthResponse InvokeAPI(
            Uri api,
            OAuthHttpMethod method,
            params QueryParameter[] queryParameters)
        {
            KeyValuesHttpBodyWrapper kvBodyContainer = new KeyValuesHttpBodyWrapper(queryParameters);
            return this.InvokeAPI(api, method, kvBodyContainer);
        }

        public OAuthResponse InvokeAPI(
            Uri api,
            OAuthHttpMethod method,
            IHttpBodyWrapper container)
        {
            if (this.Initialized)
            {
                OAuthMessage message = this.NewMessage(api, method, container);
                try
                {
                    return this.Request(message);
                }
                catch (WebException webEx)
                {
                    Console.WriteLine(new StreamReader(webEx.Response.GetResponseStream()).ReadToEnd());
                }
            }
            return null;
        }

        private OAuthMessage NewMessage(
            Uri api,
            OAuthHttpMethod method,
            IHttpBodyWrapper container)
        {
            IList<IOAuthParameter> inBodys = new List<IOAuthParameter>();
            // must a collection of keyPairs
            IList<OAuthQueryParameter> notInBodys = new List<OAuthQueryParameter>();

            // in body and need to be signed, which is must a collection of keyPairs
            IList<OAuthQueryParameter> inBodyAndNeedSign = new List<OAuthQueryParameter>();
            IList<IOAuthParameter> inBodyAndNotNeedSign = new List<IOAuthParameter>();

            IParameter[] queryParameters = container.Parameters;

            if (queryParameters != null)
            {
                foreach (IOAuthParameter ipr in queryParameters)
                {
                    if (ipr.InBody == true)
                    {
                        inBodys.Add(ipr);
                        if (ipr.NeedSign == true)
                        {
                            inBodyAndNeedSign.Add((OAuthQueryParameter)ipr);
                        }
                        else
                        {
                            inBodyAndNotNeedSign.Add(ipr);
                        }
                    }
                    else
                    {
                        notInBodys.Add((OAuthQueryParameter)ipr);
                    }
                }
            }

            string queryString = OAuthUtility.Instance.NormalizeRequestParameters(notInBodys);
            Uri baseURL = new Uri(api.AbsoluteUri + (queryString.Length == 0 ? "" : "?") + queryString.ToString());

            string timestamp = OAuthUtility.Instance.GenerateTimeStamp();
            string nonce = OAuthUtility.Instance.GenerateNonce();
            string signature =
                OAuthUtility.Instance
                .GenerateSignature(
                    baseURL,
                    this.ConsumerKey,
                    this.ConsumerSecret,
                    this.Token,
                    this.TokenSecret,
                    null,
                    method.ToString(),
                    timestamp,
                    nonce,
                    null,
                    SignatureTypes.HMACSHA1,
                    inBodyAndNeedSign.ToArray()
                    );

            signature = OAuthUtility.Instance.UrlEncode(signature);

            StringBuilder headerBuilder = new StringBuilder();
            headerBuilder
                .Append("oauth_version=\"" + OAuthUtility.OAuthVersion + "\", ")
                .Append("oauth_nonce=\"" + nonce + "\", ")
                .Append("oauth_timestamp=\"" + timestamp + "\", ")
                .Append("oauth_consumer_key=\"" + this.ConsumerKey + "\", ")
                .Append("oauth_token=\"" + this.Token + "\", ")
                .Append("oauth_signature_method=\"" + OAuthUtility.HMACSHA1SignatureType + "\", ")
                .Append("oauth_signature=\"" + signature + "\"");

            IDictionary<string, string> headers = new Dictionary<string, string>();
            headers["Authorization"] = " OAuth " + headerBuilder.ToString();
            headers["Content-Type"] = container.ContentType;

            byte[] postData = container.ToBodyData();
            return new OAuthMessage(baseURL, method, headers, postData);
        }
    }
}
